package cn.ljy.authority.service.impl;

import cn.ljy.authority.config.security.TokenConfig;
import cn.ljy.authority.dao.mapper.SysConfigMapper;
import cn.ljy.authority.dao.mapper.SysUserMapper;
import cn.ljy.authority.model.SysConfig;
import cn.ljy.authority.model.SysUser;
import cn.ljy.authority.service.SysUserTokenFrontendService;
import cn.ljy.common.exception.model.MyServiceException;
import cn.ljy.common.exception.model.MyTokenException;
import cn.ljy.common.exception.model.MyUnAuthorizedException;
import cn.ljy.common.exception.model.MyWebException;
import cn.ljy.common.utils.jwt.JwtUtils;
import cn.ljy.common.utils.password.SHA;
import cn.ljy.common.utils.redis.StringRedisUtils;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Service;
import org.springframework.util.StringUtils;

import javax.validation.constraints.NotBlank;
import java.util.HashMap;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.TimeUnit;
import java.util.stream.Collectors;

@Service
@Slf4j
public class ISysUserTokenFrontendService implements SysUserTokenFrontendService {

    @Autowired
    private TokenConfig tokenConfig;
    @Autowired
    private SysConfigMapper sysConfigMapper;
    @Autowired
    private StringRedisUtils stringRedisUtils;
    @Autowired
    private StringRedisTemplate stringRedisTemplate;
    @Autowired
    private SysUserMapper sysUserMapper;

    /**
     * 生成用户的token
     * 说明：设计为通过jwt生成
     * @param username 账户名
     * @return 生成的token
     */
    private String createToken(String username) {
        //设置要携带的信息
        Map<String,Object> claims=new HashMap<>();
        claims.put("username", username);
        return JwtUtils.creatToken(claims,tokenConfig.getJwtSecret());//生成token
    }

    /**
     * 获取用户对应的redis的key
     * @param token 客户对象的 id
     * @return
     */
    private String getRedisKey(String token){
        return tokenConfig.getAppTokenKeyHeader()+token;
    }

    /**
     * 获取key对应的token，是生成redis的key的逆向操作
     * @param redisKey key对象
     * @return token
     */
    private String getToken(String redisKey){
        String token=null;
        int index=redisKey.indexOf(tokenConfig.getAppTokenKeyHeader());
        if(index>=0){
            token=redisKey.substring(index+tokenConfig.getAppTokenKeyHeader().length());
        }else{
            throw new MyServiceException("操作失败：redis的key值格式错误");
        }
        return token;
    }

    /**
     * 从token中读取账户名
     * @param token
     * @return
     */
    private String getUsername(String token) {
        //从token中读取账户信息
        return JwtUtils.getClaimByName(token,"username").asString();
    }

    /**
     * 存入数据库
     * @param key
     * @param value
     */
    private void saveRedis(String key,String value){
        stringRedisUtils.set(key,value,tokenConfig.getAppExpireSeconds(), TimeUnit.SECONDS);
    }

    /**
     * 延期
     * @param key
     */
    private void expireRedis(String key){
        stringRedisUtils.expire(key,tokenConfig.getAppExpireSeconds(), TimeUnit.SECONDS);
    }


    @Override
    public String loginAndGetToken(String username, String password, String client) {
        String token=null;
        if(!StringUtils.hasLength(username)){
            throw new MyWebException("登录失败：请输入账户名");
        }else if(!StringUtils.hasLength(password)){
            throw new MyWebException("登录失败：请输入密码");
        }else{
            if(sysUserMapper.countByUsernameAndPassword(username, SHA.getResult(password))==1){//如果登录成功
                //读取网站配置
                SysConfig config = sysConfigMapper.getByEnable();
                //设置redis中的key
                String key=this.getRedisKey(this.createToken(username));
                //获取redis中所有前台登录用户的信息
                Set<String> keys = stringRedisTemplate.keys(tokenConfig.getAppTokenKeyHeader().concat("*"));//获取redis中指定前缀开头的key
                //筛选本账户名登录并存储在redis中的信息
                keys=keys.stream().filter(keyOfEach->this.getUsername(this.getToken(keyOfEach)).equals(username)).collect(Collectors.toSet());
                if(keys.size()>0){//如果用户登录过
                    if (config.getAllowLoginInTheSameTimeOfFrontend()) {//如果允许在多个客户端同时登录
                        //重新生成一个token存入
                        token=this.getToken(key);
                        this.saveRedis(key,client);
                        if (config.getAllowLoginInTheSameTimeOfTheSameFrontend()) {//如果允许在相同类型的客户端登录
                            //重新生成一个token存入
                            token=this.getToken(key);
                            this.saveRedis(key,client);
                        }else{//如果不允许在相同类型的客户端登录
                            //先遍历该用户在reids中的登录信息，移除该用户所有在相同类型客户端的登录信息。例如在移动端登录，则移除之前的所有移动端登录信息
                            for(String keyOfEach:keys){
                                //获取该用户原来登录的客户端
                                String clientOrigin=stringRedisUtils.get(keyOfEach);
                                if(clientOrigin.equals(client)) { //如果该用户已经在某个端登录（例如移动端登录，此时又在另一个移动端登录）
                                    //1.移除原来已经登录的账户信息
                                    stringRedisUtils.remove(keyOfEach);
                                }
                            }
                            //重新生成一个token存入
                            token=this.getToken(key);
                            this.saveRedis(key,client);
                        }
                    }else{//如果不允许在多个客户端同时登录
                        //1.移除所有登录账户的信息
                        for(String keyOfEach:keys){
                            stringRedisUtils.remove(keyOfEach);
                        }
                        //2.重新生成一个token存入
                        token=this.getToken(key);
                        this.saveRedis(key,client);
                    }
                }else{//如果用户没有登录过
                    token=this.getToken(key);
                    //重新生成一个token存入
                    this.saveRedis(key,client);
                }
            }else{
                throw new MyServiceException("登录失败：密码错误");
            }
        }
        return token;
    }


    /**
     * 验证token是否有效
     * 说明：
     * 1.首先验证token是否合法
     * 2.验证token是否在redis过期
     * 3.验证密码是否已经修改
     * @param token
     */
    private void verifyUserToken(String token) {
        //验证token是否合法
        JwtUtils.verifyToken(token,tokenConfig.getJwtSecret());
        //设置redis中的key
        String key=this.getRedisKey(token);
        //从redis中读取value
        if(!stringRedisUtils.exists(key)) {//如果key还存在，即没有过期
            throw new MyTokenException("token已失效，请重新登录");
        }
    }

    @Override
    public void verifyAndRefreshUserToken(String token) {
        //验证token是否合法
        verifyUserToken(token);
        //获取key
        String key=this.getRedisKey(token);
        //刷新key的时间为最长续航
        this.expireRedis(key);
    }

    @Override
    public void removeUserToken(String token) {
        if(StringUtils.hasLength(token)){//如果token不为空
            //验证token
            JwtUtils.verifyToken(token,tokenConfig.getJwtSecret());
            //移除token
            String key=this.getRedisKey(token);
            stringRedisUtils.remove(key);
        }
    }

    @Override
    public SysUser getUserFromToken(@NotBlank(message = "请先登录") String token) {
        String username=this.getUsername(token);
        SysUser user=sysUserMapper.findOneByUsername(username);
        if(user==null){
            throw new MyUnAuthorizedException("操作失败：找不到指定账户，请重新登录");
        }
        return user;
    }

    @Override
    public Long getUserIdFromToken(String token) {
        String username=this.getUsername(token);
        return sysUserMapper.findIdByUsername(username);
    }


}
